import { useRouter } from "next/router"; import { useMemo, useEffect, useRef } from "react"; import Page from "@/src/components/layouts/page"; import { getScoresTabs, SCORES_TABS, } from "@/src/features/navigation/utils/scores-tabs"; import { useAnalyticsUrlState } from "@/src/features/score-analytics/lib/analytics-url-state"; import { type ScoreOption } from "@/src/features/score-analytics/components/charts/ScoreCombobox"; import { useDashboardDateRange } from "@/src/hooks/useDashboardDateRange"; import { toAbsoluteTimeRange, getOptimalInterval, } from "@/src/utils/date-range-utils"; import { BarChart3, Loader2 } from "lucide-react"; import { api } from "@/src/utils/api"; import { ScoreAnalyticsProvider, type DataType, } from "@/src/features/score-analytics/components/ScoreAnalyticsProvider"; import { ScoreAnalyticsHeader } from "@/src/features/score-analytics/components/ScoreAnalyticsHeader"; import { ScoreAnalyticsDashboard } from "@/src/features/score-analytics/components/ScoreAnalyticsDashboard"; /** * Score Analytics V2 - Refactored Architecture * * This page uses the new Provider + Hook + Smart Cards pattern: * - ScoreAnalyticsProvider: Fetches & transforms data once, exposes via Context * - ScoreAnalyticsHeader: Score selectors, filters, time range picker * - ScoreAnalyticsDashboard: 2x2 responsive grid with 4 smart cards * - Smart Cards: Self-contained components that consume Provider context * - StatisticsCard: Summary metrics and comparison stats * - TimelineChartCard: Time series trends * - DistributionChartCard: Score distributions * - HeatmapCard: Score comparison heatmaps * */ export default function ScoresAnalyticsV2Page() { const router = useRouter(); const projectId = router.query.projectId as string; const urlStateHook = useAnalyticsUrlState(); const { state: urlState, setScore2 } = urlStateHook; const { timeRange, setTimeRange } = useDashboardDateRange(); // Fetch available scores from API const { data: scoresData, isLoading: scoresLoading, error: scoresError, } = api.scoreAnalytics.getScoreIdentifiers.useQuery( { projectId }, { enabled: !!projectId }, ); // Transform API data to ScoreOption format and sort by dataType const scoreOptions: ScoreOption[] = useMemo(() => { if (!scoresData?.scores) return []; // Define sort order for data types const typeOrder: Record = { BOOLEAN: 0, CATEGORICAL: 1, NUMERIC: 2, }; return scoresData.scores .map((score) => ({ value: score.value, name: score.name, dataType: score.dataType, source: score.source, })) .sort((a, b) => { // Sort by dataType first const typeA = typeOrder[a.dataType] ?? 999; const typeB = typeOrder[b.dataType] ?? 999; if (typeA !== typeB) return typeA - typeB; // Then by name alphabetically return a.name.localeCompare(b.name); }); }, [scoresData]); // Parse selected scores to get their data types const score1DataType = useMemo(() => { if (!urlState.score1) return undefined; const selected = scoreOptions.find((opt) => opt.value === urlState.score1); return selected?.dataType; }, [urlState.score1, scoreOptions]); // Determine which score types are compatible with score1 // Same-type pairing only: NUMERIC with NUMERIC, BOOLEAN with BOOLEAN, CATEGORICAL with CATEGORICAL const compatibleScore2DataTypes = useMemo(() => { if (!score1DataType) return undefined; // Only allow same-type pairing return [score1DataType]; }, [score1DataType]); // Clear score2 when score1's dataType changes const prevScore1DataTypeRef = useRef(undefined); useEffect(() => { // Skip on initial render if (prevScore1DataTypeRef.current === undefined) { prevScore1DataTypeRef.current = score1DataType; return; } // If dataType has changed and there's a score2 selected, clear it if (prevScore1DataTypeRef.current !== score1DataType && urlState.score2) { setScore2(undefined); } prevScore1DataTypeRef.current = score1DataType; }, [score1DataType, urlState.score2, setScore2]); // Parse score identifiers (format: "name-dataType-source") const parsedScore1 = useMemo(() => { if (!urlState.score1) return undefined; const selected = scoreOptions.find((opt) => opt.value === urlState.score1); if (!selected) return undefined; return { name: selected.name, dataType: selected.dataType as DataType, source: selected.source, }; }, [urlState.score1, scoreOptions]); const parsedScore2 = useMemo(() => { if (!urlState.score2) return undefined; const selected = scoreOptions.find((opt) => opt.value === urlState.score2); if (!selected) return undefined; return { name: selected.name, dataType: selected.dataType as DataType, source: selected.source, }; }, [urlState.score2, scoreOptions]); // Convert time range to absolute dates const absoluteTimeRange = useMemo( () => toAbsoluteTimeRange(timeRange), [timeRange], ); // Calculate optimal interval based on time range const interval = useMemo(() => { if (!absoluteTimeRange) return { count: 1, unit: "day" as const }; return getOptimalInterval(absoluteTimeRange.from, absoluteTimeRange.to); }, [absoluteTimeRange]); // Determine query params for Provider const queryParams = useMemo(() => { if ( !parsedScore1 || !projectId || !absoluteTimeRange?.from || !absoluteTimeRange?.to ) { return undefined; } return { projectId, score1: parsedScore1, score2: parsedScore2, fromTimestamp: absoluteTimeRange.from, toTimestamp: absoluteTimeRange.to, interval, objectType: urlState.objectType, }; }, [ parsedScore1, parsedScore2, projectId, absoluteTimeRange, interval, urlState.objectType, ]); // UI state flags const hasError = !!scoresError; const hasNoScores = !scoresLoading && scoreOptions.length === 0 && !scoresError; const hasNoSelection = !hasError && !hasNoScores && !urlState.score1; return (
{/* Header Controls */} {!hasError && !hasNoScores && ( )} {/* Content Section */}
{hasError ? (

Error Loading Scores

Failed to load score data. Please try refreshing the page.

) : hasNoScores ? (

No Scores Available

Create scores by adding evaluations to your traces and observations.

) : hasNoSelection ? (

Select a Score

Choose one or two scores from the dropdowns above to view analytics

Single score selected:

View distribution and trends over time

Two scores selected:

Compare scores with heatmaps, correlation analysis, and statistical metrics

) : queryParams ? ( ) : (

Loading analytics data...

)}
); }